feat(AssetsController): Measure intial fetch duration#7871
Conversation
6ce09f9 to
ddf7448
Compare
| hasPriceSubscription: this.#activeSubscriptions.has('ds:PriceDataSource'), | ||
| }); | ||
|
|
||
| this.#firstInitFetchReported = false; |
There was a problem hiding this comment.
Session metric resets on preference toggle
Medium Severity
#firstInitFetchReported is reset inside #stop(), but #stop() is also called by handleBasicFunctionalityChange(). That allows trackMetaMetricsEvent to fire again without a lock/unlock cycle, so the “first init fetch once per session” guarantee in AssetsController can be violated.
Additional Locations (1)
There was a problem hiding this comment.
This is not a problem
| */ | ||
| export type AssetsControllerFirstInitFetchMetaMetricsPayload = { | ||
| /** Duration of the first init fetch in milliseconds (wall-clock). */ | ||
| durationMs: number; |
There was a problem hiding this comment.
is this a duration for all fetch we do ?
| * Exclusive latency in ms per data source (time spent in that source only). | ||
| * Sum of values approximates durationMs. Order: same as middleware chain. | ||
| */ | ||
| durationByDataSource: Record<string, number>; |
There was a problem hiding this comment.
and i guess this is duration per data source
| ) => void; | ||
|
|
||
| /** Whether we have already reported first init fetch for this session (reset on #stop). */ | ||
| #firstInitFetchReported = false; |
There was a problem hiding this comment.
what this means ? is this reset after each open/close of the app or when the background stop and restart ?
salimtb
left a comment
There was a problem hiding this comment.
approving this PR , but it will be great if you can create a preview build and test it on the UI
| readonly name = CONTROLLER_NAME; | ||
|
|
||
| getName(): string { | ||
| return this.name; | ||
| } |
There was a problem hiding this comment.
nit - thoughts on.
- Making the name a static property, we never override this so it can remain static.
- We can then remove the method since we can grab the name from the static class.
export class PriceDataSource {
static readonly controllerName = CONTROLLER_NAME
}
// Elsewhere
const foo = PriceDataSource.controllerNameThere was a problem hiding this comment.
Or... technically you don't need a name right? Since the class prototype already takes the name of the class?
// The class prototype already has a name of 'PriceDataSource'
export class PriceDataSource {
}
// Elsewhere
const foo = PriceDataSource.nameThere was a problem hiding this comment.
Okay, I see the implementation below, we want instances to grab the name. Still we can clean this method up by making it point to the static fields (instead of creating a name for every instantiation?)
getName(): string {
return PriceDataSource.controllerName
}
// or use Class name prototype
getName(): string {
return PriceDataSource.name
}ddf7448 to
06eb670
Compare
Yeah I've tested it with MetaMask/metamask-extension#40087 |
| durationMs, | ||
| chainIds, | ||
| durationByDataSource, | ||
| }); |
There was a problem hiding this comment.
Stale fetch can misreport session metric
Medium Severity
trackMetaMetricsEvent is emitted based only on #firstInitFetchReported, with no guard that the fetch still belongs to the active session. If #stop() runs while a prior getAssets(..., forceUpdate) is still in flight, that stale fetch can report after stop and set the flag, skewing session attribution and suppressing the next real init metric.
Additional Locations (1)
## Explanation <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> * **Current state / why change:** AssetsController runs an initial asset fetch when the user unlocks the wallet or when the app opens. There is no way to report how long that first fetch takes to our analytics. We need to measure this (InitMetrics) so we can track performance in MetaMetrics and spot regressions or improvements in the assets pipeline. * **Solution / how it works:** An optional `trackMetaMetricsEvent` callback was added to the AssetsController constructor. When the **first** init/fetch completes (after unlock or app open), the controller invokes this callback once per session with a payload: `durationMs` (wall-clock duration in ms), `chainIds` (requested chains, e.g. `['eip155:1', 'eip155:137']`), and `durationByDataSource` (exclusive latency per data source, same order as the middleware chain). A new type `AssetsControllerFirstInitFetchMetaMetricsPayload` was added and exported so consumers can type the callback. The callback is only fired once per “session”; the internal “reported” flag is reset in `#stop()` (e.g. on lock), so the next unlock can trigger the event again. Consumers (e.g. the extension) can pass a callback that forwards this payload to MetaMetricsController; the controller does not depend on MetaMetrics directly. * **Non-obvious changes:** `durationByDataSource` is exclusive latency per data source (sum approximates `durationMs`). This allows downstream analytics to see which data sources contribute most to init time. * **Other packages / dependency upgrades:** None; changes are confined to `@metamask/assets-controller`. ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> * (Add issue numbers or related PRs if applicable; e.g. Fixes #XXXX, or link to extension/mobile PR that wires up `trackMetaMetricsEvent`.) ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches the core asset fetch pipeline and start/stop lifecycle by refactoring middleware execution and adding new timing/analytics hooks, which could affect initial fetch behavior and ordering if incorrect. > > **Overview** > Adds an optional `trackMetaMetricsEvent` callback to `AssetsController` that fires **once per unlock/app-open session** after the first `getAssets({ forceUpdate: true })` completes, reporting total duration, requested `chainIds`, and a per-source `durationByDataSource` breakdown. > > To support this, middleware execution is refactored to accept sources with `getName()` and to measure/return exclusive timings per data source/middleware; `TokenDataSource`, `DetectionMiddleware`, and `PriceDataSource` now expose `getName()` (and `PriceDataSource` no longer relies on an instance `name` field). Tests and exports are updated, and the “first init reported” flag resets on `#stop()` (lock) so the metric can be emitted again next session. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 01f41a9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->


Explanation
Current state / why change: AssetsController runs an initial asset fetch when the user unlocks the wallet or when the app opens. There is no way to report how long that first fetch takes to our analytics. We need to measure this (InitMetrics) so we can track performance in MetaMetrics and spot regressions or improvements in the assets pipeline.
Solution / how it works: An optional
trackMetaMetricsEventcallback was added to the AssetsController constructor. When the first init/fetch completes (after unlock or app open), the controller invokes this callback once per session with a payload:durationMs(wall-clock duration in ms),chainIds(requested chains, e.g.['eip155:1', 'eip155:137']), anddurationByDataSource(exclusive latency per data source, same order as the middleware chain). A new typeAssetsControllerFirstInitFetchMetaMetricsPayloadwas added and exported so consumers can type the callback. The callback is only fired once per “session”; the internal “reported” flag is reset in#stop()(e.g. on lock), so the next unlock can trigger the event again. Consumers (e.g. the extension) can pass a callback that forwards this payload to MetaMetricsController; the controller does not depend on MetaMetrics directly.Non-obvious changes:
durationByDataSourceis exclusive latency per data source (sum approximatesdurationMs). This allows downstream analytics to see which data sources contribute most to init time.Other packages / dependency upgrades: None; changes are confined to
@metamask/assets-controller.References
trackMetaMetricsEvent.)Checklist
Note
Medium Risk
Touches the core asset fetch pipeline and start/stop lifecycle by refactoring middleware execution and adding new timing/analytics hooks, which could affect initial fetch behavior and ordering if incorrect.
Overview
Adds an optional
trackMetaMetricsEventcallback toAssetsControllerthat fires once per unlock/app-open session after the firstgetAssets({ forceUpdate: true })completes, reporting total duration, requestedchainIds, and a per-sourcedurationByDataSourcebreakdown.To support this, middleware execution is refactored to accept sources with
getName()and to measure/return exclusive timings per data source/middleware;TokenDataSource,DetectionMiddleware, andPriceDataSourcenow exposegetName()(andPriceDataSourceno longer relies on an instancenamefield). Tests and exports are updated, and the “first init reported” flag resets on#stop()(lock) so the metric can be emitted again next session.Written by Cursor Bugbot for commit 01f41a9. This will update automatically on new commits. Configure here.